package core.domain.query.paging;

import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceException;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Expression;
import javax.persistence.criteria.Order;
import javax.persistence.criteria.Predicate;

import org.apache.commons.lang.Validate;
import org.hibernate.annotations.QueryHints;
import org.springframework.orm.jpa.JpaCallback;
import org.springframework.util.CollectionUtils;

import core.domain.IdEntity;
import core.domain.query.expgen.JpaSearchExpGenerator;


@SuppressWarnings("all")
public class JPAPagingCallback<T extends IdEntity> implements JpaCallback<Pageable<T>> {

    private Class<T> entityClass;

    private JpaSearchExpGenerator<T> generator;

    private PagingBean pagingBean;

    public JPAPagingCallback(Class<T> clazz, JpaSearchExpGenerator<T> generator, PagingBean pb) {
        super();
        this.entityClass = clazz;
        this.generator = generator;
        this.pagingBean = pb;
    }

    /**
     * select_statement ::= select [DISTINCT] select_expression from_clause [where_clause] [groupby_clause]
     * [having_clause] [orderby_clause]
     */
    public Pageable<T> doInJpa(EntityManager em) throws PersistenceException {
        Validate.notNull(em);
        Validate.notNull(generator);
        Validate.notNull(pagingBean);

        CriteriaBuilder cb = em.getCriteriaBuilder();

        // data result query
        CriteriaQuery<T> dataCriteriaQuery = cb.createQuery(entityClass);

        Predicate searchPredicate = generator.generate(cb, dataCriteriaQuery, em);

        boolean distinctable = generator.getDistinctable();

        dataCriteriaQuery.distinct(distinctable);

        if (null != searchPredicate) {
            dataCriteriaQuery.where(searchPredicate);
        }

        List<Expression<?>> groupBy = generator.getGroupBy(cb);
        if (!CollectionUtils.isEmpty(groupBy)) {
            dataCriteriaQuery.groupBy(groupBy);
        }

        List<Predicate> having = generator.having(cb);
        if (!CollectionUtils.isEmpty(having)) {
            dataCriteriaQuery.having(having.toArray(new Predicate[having.size()]));
        }

        List<Order> orderBy = generator.getOrderBy(cb);
        if (!CollectionUtils.isEmpty(orderBy)) {
            dataCriteriaQuery.orderBy(orderBy);
        }

        TypedQuery<T> dataQuery = em.createQuery(dataCriteriaQuery);

        // TODO: Set the hints.
//        Map<String, Object> queryHints = generator.getQueryHints();
//        if (!(queryHints == null || queryHints.keySet().size() == 0)) {
//            for (Entry<String, Object> entry : queryHints.entrySet()) {
//                if (QueryHints.BATCH.equals(entry.getKey())) {
//                    List<String> values = (List<String>) entry.getValue();
//                    for (String value : values) {
//                        dataQuery.setHint(entry.getKey(), value);
//                    }
//                } else {
//                    dataQuery.setHint(entry.getKey(), entry.getValue());
//                }
//
//            }
//        }

        CriteriaQuery<Long> countCriteriaQuery = cb.createQuery(Long.class);

        Expression<Long> countExp = distinctable ? cb.countDistinct(generator.getRoot()) : cb.count(generator
                .getRoot());

        countCriteriaQuery.select(countExp);
        countCriteriaQuery.from(entityClass);

        if (null != searchPredicate) {
            countCriteriaQuery.where(searchPredicate);
        }

        if (CollectionUtils.isEmpty(having)) {
            countCriteriaQuery.having(having.toArray(new Predicate[having.size()]));
        }

        if (!CollectionUtils.isEmpty(orderBy)) {
            countCriteriaQuery.orderBy(orderBy);
        }

        TypedQuery<Long> countQuery = em.createQuery(countCriteriaQuery);

        Pageable<T> cursor = new JpaQueryPagingCursor<T>(dataQuery, countQuery, pagingBean.getPageSize());
        if (0 != pagingBean.getPageStart()) {
            cursor.to(pagingBean.getPageStart());
        }

        return cursor;
    }

}
